<!DOCTYPE html><html lang="en" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0,viewport-fit=cover"><title>MySQL索引 | 牵风散步の雲</title><meta name="author" content="牵风散步の雲"><meta name="copyright" content="牵风散步の雲"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="ffffff"><meta name="description" content="MySQL索引篇1. 从数据页的角度看 B+ 树 InnoDB 是如何存储数据的？MySQL 支持多种存储引擎，不同的存储引擎，存储数据的方式也是不同的，我们最常使用的是 InnoDB 存储引擎。 记录是按照行来存储的，但是数据库的读取并不以「行」为单位，否则一次读取（也就是一次 I&#x2F;O 操作）只能处理一行数据，效率会非常低。 因此，InnoDB 的数据是按「数据页」为单位来读写的，也就">
<meta property="og:type" content="article">
<meta property="og:title" content="MySQL索引">
<meta property="og:url" content="https://kirito2493381254.gitee.io/kirito/2023/08/09/MySQL%E7%B4%A2%E5%BC%95%E7%AF%87/index.html">
<meta property="og:site_name" content="牵风散步の雲">
<meta property="og:description" content="MySQL索引篇1. 从数据页的角度看 B+ 树 InnoDB 是如何存储数据的？MySQL 支持多种存储引擎，不同的存储引擎，存储数据的方式也是不同的，我们最常使用的是 InnoDB 存储引擎。 记录是按照行来存储的，但是数据库的读取并不以「行」为单位，否则一次读取（也就是一次 I&#x2F;O 操作）只能处理一行数据，效率会非常低。 因此，InnoDB 的数据是按「数据页」为单位来读写的，也就">
<meta property="og:locale" content="en_US">
<meta property="og:image" content="https://kirito2493381254.gitee.io/kirito/img/artical5.jpg">
<meta property="article:published_time" content="2023-08-09T08:20:58.443Z">
<meta property="article:modified_time" content="2023-08-09T11:49:32.763Z">
<meta property="article:author" content="牵风散步の雲">
<meta property="article:tag" content="MySQL索引篇">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://kirito2493381254.gitee.io/kirito/img/artical5.jpg"><link rel="shortcut icon" href="/kirito/img/favicon.png"><link rel="canonical" href="https://kirito2493381254.gitee.io/kirito/2023/08/09/MySQL%E7%B4%A2%E5%BC%95%E7%AF%87/index.html"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="//busuanzi.ibruce.info"/><link rel="stylesheet" href="/kirito/css/index.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css" media="print" onload="this.media='all'"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox/fancybox.min.css" media="print" onload="this.media='all'"><script>const GLOBAL_CONFIG = {
  root: '/kirito/',
  algolia: undefined,
  localSearch: undefined,
  translate: undefined,
  noticeOutdate: undefined,
  highlight: {"plugin":"highlighjs","highlightCopy":true,"highlightLang":true,"highlightHeightLimit":false},
  copy: {
    success: 'Copy successfully',
    error: 'Copy error',
    noSupport: 'The browser does not support'
  },
  relativeDate: {
    homepage: true,
    post: true
  },
  runtime: '',
  dateSuffix: {
    just: 'Just',
    min: 'minutes ago',
    hour: 'hours ago',
    day: 'days ago',
    month: 'months ago'
  },
  copyright: undefined,
  lightbox: 'fancybox',
  Snackbar: undefined,
  source: {
    justifiedGallery: {
      js: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.js',
      css: 'https://cdn.jsdelivr.net/npm/flickr-justified-gallery/dist/fjGallery.min.css'
    }
  },
  isPhotoFigcaption: false,
  islazyload: false,
  isAnchor: false,
  percent: {
    toc: true,
    rightside: true,
  },
  autoDarkmode: false
}</script><script id="config-diff">var GLOBAL_CONFIG_SITE = {
  title: 'MySQL索引',
  isPost: true,
  isHome: false,
  isHighlightShrink: false,
  isToc: true,
  postUpdate: '2023-08-09 19:49:32'
}</script><noscript><style type="text/css">
  #nav {
    opacity: 1
  }
  .justified-gallery img {
    opacity: 1
  }

  #recent-posts time,
  #post-meta time {
    display: inline !important
  }
</style></noscript><script>(win=>{
    win.saveToLocal = {
      set: function setWithExpiry(key, value, ttl) {
        if (ttl === 0) return
        const now = new Date()
        const expiryDay = ttl * 86400000
        const item = {
          value: value,
          expiry: now.getTime() + expiryDay,
        }
        localStorage.setItem(key, JSON.stringify(item))
      },

      get: function getWithExpiry(key) {
        const itemStr = localStorage.getItem(key)

        if (!itemStr) {
          return undefined
        }
        const item = JSON.parse(itemStr)
        const now = new Date()

        if (now.getTime() > item.expiry) {
          localStorage.removeItem(key)
          return undefined
        }
        return item.value
      }
    }
  
    win.getScript = url => new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.src = url
      script.async = true
      script.onerror = reject
      script.onload = script.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        script.onload = script.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(script)
    })
  
    win.getCSS = (url,id = false) => new Promise((resolve, reject) => {
      const link = document.createElement('link')
      link.rel = 'stylesheet'
      link.href = url
      if (id) link.id = id
      link.onerror = reject
      link.onload = link.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        link.onload = link.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(link)
    })
  
      win.activateDarkMode = function () {
        document.documentElement.setAttribute('data-theme', 'dark')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0d0d0d')
        }
      }
      win.activateLightMode = function () {
        document.documentElement.setAttribute('data-theme', 'light')
        if (document.querySelector('meta[name="theme-color"]') !== null) {
          document.querySelector('meta[name="theme-color"]').setAttribute('content', 'ffffff')
        }
      }
      const t = saveToLocal.get('theme')
    
          if (t === 'dark') activateDarkMode()
          else if (t === 'light') activateLightMode()
        
      const asideStatus = saveToLocal.get('aside-status')
      if (asideStatus !== undefined) {
        if (asideStatus === 'hide') {
          document.documentElement.classList.add('hide-aside')
        } else {
          document.documentElement.classList.remove('hide-aside')
        }
      }
    
    const detectApple = () => {
      if(/iPad|iPhone|iPod|Macintosh/.test(navigator.userAgent)){
        document.documentElement.classList.add('apple')
      }
    }
    detectApple()
    })(window)</script><meta name="generator" content="Hexo 6.3.0"><link rel="alternate" href="/kirito/atom.xml" title="牵风散步の雲" type="application/atom+xml">
</head><body><div id="web_bg"></div><div id="sidebar"><div id="menu-mask"></div><div id="sidebar-menus"><div class="avatar-img is-center"><img src="/kirito/img/avatar.jpg" onerror="onerror=null;src='/img/friend_404.gif'" alt="avatar"/></div><div class="sidebar-site-data site-data is-center"><a href="/kirito/archives/"><div class="headline">Articles</div><div class="length-num">21</div></a><a href="/kirito/tags/"><div class="headline">Tags</div><div class="length-num">12</div></a><a href="/kirito/categories/"><div class="headline">Categories</div><div class="length-num">8</div></a></div><hr class="custom-hr"/><div class="menus_items"><div class="menus_item"><a class="site-page" href="/kirito/"><i class="fa-fw fas fa-home"></i><span> 首頁</span></a></div><div class="menus_item"><a class="site-page" href="/kirito/archives/"><i class="fa-fw fas fa-archive"></i><span> 時間軸</span></a></div><div class="menus_item"><a class="site-page" href="/kirito/tags/"><i class="fa-fw fas fa-tags"></i><span> 標籤</span></a></div><div class="menus_item"><a class="site-page" href="/kirito/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分類</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fa fa-heartbeat"></i><span> 清單</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/kirito/Gallery/"><i class="fa-fw fas fa-images"></i><span> 照片</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/kirito/link/"><i class="fa-fw fas fa-link"></i><span> 友鏈</span></a></div></div></div></div><div class="post" id="body-wrap"><header class="post-bg" id="page-header" style="background-image: url('/kirito/img/artical5.jpg')"><nav id="nav"><span id="blog-info"><a href="/kirito/" title="牵风散步の雲"><span class="site-name">牵风散步の雲</span></a></span><div id="menus"><div class="menus_items"><div class="menus_item"><a class="site-page" href="/kirito/"><i class="fa-fw fas fa-home"></i><span> 首頁</span></a></div><div class="menus_item"><a class="site-page" href="/kirito/archives/"><i class="fa-fw fas fa-archive"></i><span> 時間軸</span></a></div><div class="menus_item"><a class="site-page" href="/kirito/tags/"><i class="fa-fw fas fa-tags"></i><span> 標籤</span></a></div><div class="menus_item"><a class="site-page" href="/kirito/categories/"><i class="fa-fw fas fa-folder-open"></i><span> 分類</span></a></div><div class="menus_item"><a class="site-page group" href="javascript:void(0);"><i class="fa-fw fa fa-heartbeat"></i><span> 清單</span><i class="fas fa-chevron-down"></i></a><ul class="menus_item_child"><li><a class="site-page child" href="/kirito/Gallery/"><i class="fa-fw fas fa-images"></i><span> 照片</span></a></li></ul></div><div class="menus_item"><a class="site-page" href="/kirito/link/"><i class="fa-fw fas fa-link"></i><span> 友鏈</span></a></div></div><div id="toggle-menu"><a class="site-page" href="javascript:void(0);"><i class="fas fa-bars fa-fw"></i></a></div></div></nav><div id="post-info"><h1 class="post-title">MySQL索引</h1><div id="post-meta"><div class="meta-firstline"><span class="post-meta-date"><i class="far fa-calendar-alt fa-fw post-meta-icon"></i><span class="post-meta-label">Created</span><time class="post-meta-date-created" datetime="2023-08-09T08:20:58.443Z" title="Created 2023-08-09 16:20:58">2023-08-09</time><span class="post-meta-separator">|</span><i class="fas fa-history fa-fw post-meta-icon"></i><span class="post-meta-label">Updated</span><time class="post-meta-date-updated" datetime="2023-08-09T11:49:32.763Z" title="Updated 2023-08-09 19:49:32">2023-08-09</time></span><span class="post-meta-categories"><span class="post-meta-separator">|</span><i class="fas fa-inbox fa-fw post-meta-icon"></i><a class="post-meta-categories" href="/kirito/categories/MySQL/">MySQL</a></span></div><div class="meta-secondline"><span class="post-meta-separator">|</span><span class="post-meta-pv-cv" id="" data-flag-title="MySQL索引"><i class="far fa-eye fa-fw post-meta-icon"></i><span class="post-meta-label">Post View:</span><span id="busuanzi_value_page_pv"><i class="fa-solid fa-spinner fa-spin"></i></span></span></div></div></div></header><main class="layout" id="content-inner"><div id="post"><article class="post-content" id="article-container"><h1 id="MySQL索引篇"><a href="#MySQL索引篇" class="headerlink" title="MySQL索引篇"></a>MySQL索引篇</h1><h2 id="1-从数据页的角度看-B-树"><a href="#1-从数据页的角度看-B-树" class="headerlink" title="1. 从数据页的角度看 B+ 树"></a>1. 从数据页的角度看 B+ 树</h2><p><img src="https://cdn.xiaolincoding.com//mysql/other/f7696506a3c1c94621fcbad10341f1a8.png" alt="图片"></p>
<h3 id="InnoDB-是如何存储数据的？"><a href="#InnoDB-是如何存储数据的？" class="headerlink" title="InnoDB 是如何存储数据的？"></a>InnoDB 是如何存储数据的？</h3><p>MySQL 支持多种存储引擎，不同的存储引擎，存储数据的方式也是不同的，我们最常使用的是 InnoDB 存储引擎。</p>
<p>记录是按照行来存储的，但是数据库的读取并不以「行」为单位，否则一次读取（也就是一次 I&#x2F;O 操作）只能处理一行数据，效率会非常低。</p>
<p>因此，<strong>InnoDB 的数据是按「数据页」为单位来读写的</strong>，也就是说，当需要读一条记录的时候，并不是将这个记录本身从磁盘读出来，而是以页为单位，将其整体读入内存。</p>
<p>数据库的 I&#x2F;O 操作的最小单位是页，<strong>InnoDB 数据页的默认大小是 16KB</strong>，意味着数据库每次读写都是以 16KB 为单位的，一次最少从磁盘中读取 16K 的内容到内存中，一次最少把内存中的 16K 内容刷新到磁盘中。</p>
<p>数据页包括七个部分，结构如下图：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/243b1466779a9e107ae3ef0155604a17.png" alt="图片"></p>
<p>在 File Header 中有两个指针，分别指向上一个数据页和下一个数据页，连接起来的页相当于一个双向的链表，如下图所示：</p>
<p><img src="/kirito/kirito/2023/08/09/MySQL%E7%B4%A2%E5%BC%95%E7%AF%87/MySQL/1691569695919.png" alt="1691569695919"></p>
<p>采用链表的结构是让数据页之间不需要是物理上的连续的，而是逻辑上的连续。</p>
<p>数据页的主要作用是存储记录，也就是数据库的数据，所以重点说一下数据页中的 User Records 是怎么组织数据的。</p>
<p><strong>数据页中的记录按照「主键」顺序组成单向链表</strong>，单向链表的特点就是插入、删除非常方便，但是检索效率不高，最差的情况下需要遍历链表上的所有节点才能完成检索。</p>
<p>因此，数据页中有一个<strong>页目录</strong>，起到记录的索引作用，就像我们书那样，针对书中内容的每个章节设立了一个目录，想看某个章节的时候，可以查看目录，快速找到对应的章节的页数，而数据页中的页目录就是为了能快速找到记录。</p>
<p>那 InnoDB 是如何给记录创建页目录的呢？页目录与记录的关系如下图：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/261011d237bec993821aa198b97ae8ce.png" alt="图片"></p>
<p>页目录创建的过程如下：</p>
<ol>
<li>将所有的记录划分成几个组，这些记录包括最小记录和最大记录，但不包括标记为“已删除”的记录；</li>
<li>每个记录组的最后一条记录就是组内最大的那条记录，并且最后一条记录的头信息中会存储该组一共有多少条记录，作为 n_owned 字段（上图中粉红色字段）</li>
<li>页目录用来存储每组最后一条记录的地址偏移量，这些地址偏移量会按照先后顺序存储起来，每组的地址偏移量也被称之为槽（slot），<strong>每个槽相当于指针指向了不同组的最后一个记录</strong>。</li>
</ol>
<p>从图可以看到，<strong>页目录就是由多个槽组成的，槽相当于分组记录的索引</strong>。然后，因为记录是按照「主键值」从小到大排序的，所以<strong>我们通过槽查找记录时，可以使用二分法快速定位要查询的记录在哪个槽（哪个记录分组），定位到槽后，再遍历槽内的所有记录，找到对应的记录</strong>，无需从最小记录开始遍历整个页中的记录链表。</p>
<p>以上面那张图举个例子，5 个槽的编号分别为 0，1，2，3，4，我想查找主键为 11 的用户记录：</p>
<ul>
<li>先二分得出槽中间位是 (0+4)&#x2F;2&#x3D;2 ，2号槽里最大的记录为 8。因为 11 &gt; 8，所以需要从 2 号槽后继续搜索记录；</li>
<li>再使用二分搜索出 2 号和 4 槽的中间位是 (2+4)&#x2F;2&#x3D; 3，3 号槽里最大的记录为 12。因为 11 &lt; 12，所以主键为 11 的记录在 3 号槽里；</li>
<li>这里有个问题，<strong>「槽对应的值都是这个组的主键最大的记录，如何找到组里最小的记录」</strong>？比如槽 3 对应最大主键是 12 的记录，那如何找到最小记录 9。解决办法是：通过槽 2 找到 槽 3 对应的记录，也就是主键为 8 的记录。主键为 8 的记录的下一条记录就是槽 3 当中主键最小的 9 记录，然后开始向下搜索 2 次，定位到主键为 11 的记录，取出该条记录的信息即为我们想要查找的内容。</li>
</ul>
<p>看到第三步的时候，可能有的同学会疑问，如果某个槽内的记录很多，然后因为记录都是单向链表串起来的，那这样在槽内查找某个记录的时间复杂度不就是 O(n) 了吗？</p>
<p>这点不用担心，InnoDB 对每个分组中的记录条数都是有规定的，槽内的记录就只有几条：</p>
<ul>
<li>第一个分组中的记录只能有 1 条记录；</li>
<li>最后一个分组中的记录条数范围只能在 1-8 条之间；</li>
<li>剩下的分组中记录条数范围只能在 4-8 条之间。</li>
</ul>
<h3 id="B-树是如何进行查询的？"><a href="#B-树是如何进行查询的？" class="headerlink" title="B+ 树是如何进行查询的？"></a>B+ 树是如何进行查询的？</h3><p>上面我们都是在说一个数据页中的记录检索，因为一个数据页中的记录是有限的，且主键值是有序的，所以通过对所有记录进行分组，然后将组号（槽号）存储到页目录，使其起到索引作用，通过二分查找的方法快速检索到记录在哪个分组，来降低检索的时间复杂度。</p>
<p>但是，当我们需要存储大量的记录时，就需要多个数据页，这时我们就需要考虑如何建立合适的索引，才能方便定位记录所在的页。</p>
<p>为了解决这个问题，<strong>InnoDB 采用了 B+ 树作为索引</strong>。磁盘的 I&#x2F;O 操作次数对索引的使用效率至关重要，因此在构造索引的时候，我们更倾向于采用“矮胖”的 B+ 树数据结构，这样所需要进行的磁盘 I&#x2F;O 次数更少，而且 B+ 树 更适合进行关键字的范围查询。</p>
<p>InnoDB 里的 B+ 树中的<strong>每个节点都是一个数据页</strong>，结构示意图如下：</p>
<p><img src="/kirito/kirito/2023/08/09/MySQL%E7%B4%A2%E5%BC%95%E7%AF%87/MySQL/1691570550414.png" alt="1691570550414"></p>
<p>通过上图，我们看出 B+ 树的特点：</p>
<ul>
<li>只有叶子节点（最底层的节点）才存放了数据，非叶子节点（其他上层节）仅用来存放目录项作为索引。</li>
<li>非叶子节点分为不同层次，通过分层来降低每一层的搜索量；</li>
<li>所有节点按照索引键大小排序，构成一个双向链表，便于范围查询；</li>
</ul>
<p>我们再看看 B+ 树如何实现快速查找主键为 6 的记录，以上图为例子：</p>
<ul>
<li>从根节点开始，通过二分法快速定位到符合页内范围包含查询值的页，因为查询的主键值为 6，在[1, 7)范围之间，所以到页 30 中查找更详细的目录项；</li>
<li>在非叶子节点（页30）中，继续通过二分法快速定位到符合页内范围包含查询值的页，主键值大于 5，所以就到叶子节点（页16）查找记录；</li>
<li>接着，在叶子节点（页16）中，通过槽查找记录时，使用二分法快速定位要查询的记录在哪个槽（哪个记录分组），定位到槽后，再遍历槽内的所有记录，找到主键为 6 的记录。</li>
</ul>
<p>可以看到，在定位记录所在哪一个页时，也是通过二分法快速定位到包含该记录的页。定位到该页后，又会在该页内进行二分法快速定位记录所在的分组（槽号），最后在分组内进行遍历查找。</p>
<h3 id="聚簇索引和二级索引"><a href="#聚簇索引和二级索引" class="headerlink" title="聚簇索引和二级索引"></a>聚簇索引和二级索引</h3><p>另外，索引又可以分成聚簇索引和非聚簇索引（二级索引），它们区别就在于叶子节点存放的是什么数据：</p>
<ul>
<li>聚簇索引的叶子节点存放的是实际数据，所有完整的用户记录都存放在聚簇索引的叶子节点；</li>
<li>二级索引的叶子节点存放的是主键值，而不是实际数据。</li>
</ul>
<p>因为表的数据都是存放在聚簇索引的叶子节点里，所以 InnoDB 存储引擎一定会为表创建一个聚簇索引，且由于数据在物理上只会保存一份，所以聚簇索引只能有一个。</p>
<p>InnoDB 在创建聚簇索引时，会根据不同的场景选择不同的列作为索引：</p>
<ul>
<li>如果有主键，默认会使用主键作为聚簇索引的索引键；</li>
<li>如果没有主键，就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键；</li>
<li>在上面两个都没有的情况下，InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键；</li>
</ul>
<p>一张表只能有一个聚簇索引，那为了实现非主键字段的快速搜索，就引出了二级索引（非聚簇索引&#x2F;辅助索引），它也是利用了 B+ 树的数据结构，但是二级索引的叶子节点存放的是主键值，不是实际数据。</p>
<p>二级索引的 B+ 树如下图，数据部分为主键值：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/3104c8c3adf36e8931862fe8a0520f5d.png" alt="图片"></p>
<p>因此，<strong>如果某个查询语句使用了二级索引，但是查询的数据不是主键值，这时在二级索引找到主键值后，需要去聚簇索引中获得数据行，这个过程就叫作「回表」，也就是说要查两个 B+ 树才能查到数据。不过，当查询的数据是主键值时，因为只在二级索引就能查询到，不用再去聚簇索引查，这个过程就叫作「索引覆盖」，也就是只需要查一个 B+ 树就能找到数据。</strong></p>
<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>InnoDB 的数据是按「数据页」为单位来读写的，默认数据页大小为 16 KB。每个数据页之间通过双向链表的形式组织起来，物理上不连续，但是逻辑上连续。</p>
<p>数据页内包含用户记录，每个记录之间用单向链表的方式组织起来，为了加快在数据页内高效查询记录，设计了一个页目录，页目录存储各个槽（分组），且主键值是有序的，于是可以通过二分查找法的方式进行检索从而提高效率。</p>
<p>为了高效查询记录所在的数据页，InnoDB 采用 b+ 树作为索引，每个节点都是一个数据页。</p>
<p>如果叶子节点存储的是实际数据的就是聚簇索引，一个表只能有一个聚簇索引；如果叶子节点存储的不是实际数据，而是主键值则就是二级索引，一个表中可以有多个二级索引。</p>
<p>在使用二级索引进行查找数据时，如果查询的数据能在二级索引找到，那么就是「索引覆盖」操作，如果查询的数据不在二级索引里，就需要先在二级索引找到主键值，需要去聚簇索引中获得数据行，这个过程就叫作「回表」。</p>
<h2 id="2-为什么-MySQL-采用-B-树作为索引？"><a href="#2-为什么-MySQL-采用-B-树作为索引？" class="headerlink" title="2. 为什么 MySQL 采用 B+ 树作为索引？"></a>2. 为什么 MySQL 采用 B+ 树作为索引？</h2><p>「为什么 MySQL 采用 B+ 树作为索引？」这句话，是不是在面试时经常出现。</p>
<p>要解释这个问题，其实不单单要从数据结构的角度出发，还要考虑磁盘 I&#x2F;O 操作次数，因为 MySQL 的数据是存储在磁盘中的嘛。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/3dcb127877c7e77839404275279b136b.png" alt="图片"></p>
<h3 id="怎样的索引的数据结构是好的？"><a href="#怎样的索引的数据结构是好的？" class="headerlink" title="怎样的索引的数据结构是好的？"></a>怎样的索引的数据结构是好的？</h3><p>MySQL 的数据是持久化的，意味着数据（索引+记录）是保存到磁盘上的，因为这样即使设备断电了，数据也不会丢失。</p>
<p>磁盘是一个慢的离谱的存储设备，有多离谱呢？</p>
<p>人家内存的访问速度是纳秒级别的，而磁盘访问的速度是毫秒级别的，也就是说读取同样大小的数据，磁盘中读取的速度比从内存中读取的速度要慢上万倍，甚至几十万倍。</p>
<p>磁盘读写的最小单位是<strong>扇区</strong>，扇区的大小(一般)只有 <code>512B</code> 大小，操作系统一次会读写多个扇区，所以<strong>操作系统的最小读写单位是块（Block）。Linux 中的块大小为 4KB</strong>，也就是一次磁盘 I&#x2F;O 操作会直接读写 8 个扇区。</p>
<p>由于数据库的索引是保存到磁盘上的，因此当我们通过索引查找某行数据的时候，就需要先从磁盘读取索引到内存，再通过索引从磁盘中找到某行数据，然后读入到内存，也就是说查询过程中会发生多次磁盘 I&#x2F;O，而磁盘 I&#x2F;O 次数越多，所消耗的时间也就越大。</p>
<p>所以，我们希望索引的数据结构能在尽可能少的磁盘的 I&#x2F;O 操作中完成查询工作，因为磁盘 I&#x2F;O 操作越少，所消耗的时间也就越小。</p>
<p>另外，MySQL 是支持范围查找的，所以索引的数据结构不仅要能高效地查询某一个记录，而且也要能高效地执行范围查找。</p>
<p>所以，要设计一个适合 MySQL 索引的数据结构，至少满足以下要求：</p>
<ul>
<li>能在尽可能少的磁盘的 I&#x2F;O 操作中完成查询工作；</li>
<li>要能高效地查询某一个记录，也要能高效地执行范围查找；</li>
</ul>
<p>分析完要求后，我们针对每一个数据结构分析一下。</p>
<h3 id="什么是二分查找？"><a href="#什么是二分查找？" class="headerlink" title="什么是二分查找？"></a>什么是二分查找？</h3><p>索引数据最好能按顺序排列，这样可以使用「二分查找法」高效定位数据。</p>
<p>假设我们现在用数组来存储索引，比如下面有一个排序的数组，如果要从中找出数字 3，最简单办法就是从头依次遍历查询，这种方法的时间复杂度是 O(n)，查询效率并不高。因为该数组是有序的，所以我们可以采用二分查找法，比如下面这张采用二分法的查询过程图：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/f01bb5e7e940231c4f39e7f1cfb449f3.png" alt="图片"></p>
<p>可以看到，二分查找法每次都把查询的范围减半，这样时间复杂度就降到了 O(logn)，但是每次查找都需要不断计算中间位置。</p>
<h3 id="什么是二分查找树？"><a href="#什么是二分查找树？" class="headerlink" title="什么是二分查找树？"></a>什么是二分查找树？</h3><p>用数组来实现线性排序的数据虽然简单好用，但是插入新元素的时候性能太低。</p>
<p>因为插入一个元素，需要将这个元素之后的所有元素后移一位，如果这个操作发生在磁盘中呢？这必然是灾难性的。因为磁盘的速度比内存慢几十万倍，所以我们不能用一种线性结构将磁盘排序。</p>
<p>其次，有序的数组在使用二分查找的时候，每次查找都要不断计算中间的位置。</p>
<p>那我们能不能设计一个非线形且天然适合二分查找的数据结构呢？</p>
<p>有的，请看下图这个神奇的操作，找到所有二分查找中用到的所有中间节点，把他们用指针连起来，并将最中间的节点作为根节点。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/49cb4df3fb7d4accbc75f6970ff1d0ea.gif" alt="请添加图片描述"></p>
<p>怎么样？是不是变成了二叉树，不过它不是普通的二叉树，它是一个<strong>二叉查找树</strong>。</p>
<p><strong>二叉查找树的特点是一个节点的左子树的所有节点都小于这个节点，右子树的所有节点都大于这个节点</strong>，这样我们在查询数据时，不需要计算中间节点的位置了，只需将查找的数据与节点的数据进行比较。</p>
<p>假设，我们查找索引值为 key 的节点：</p>
<ol>
<li>如果 key 大于根节点，则在右子树中进行查找；</li>
<li>如果 key 小于根节点，则在左子树中进行查找；</li>
<li>如果 key 等于根节点，也就是找到了这个节点，返回根节点即可。</li>
</ol>
<p>二叉查找树查找某个节点的动图演示如下，比如要查找节点 3 ：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/f3089bdfe1a795a8142d4442bdb1e9a7.gif" alt="图片"></p>
<p>另外，二叉查找树解决了插入新节点的问题，因为二叉查找树是一个跳跃结构，不必连续排列。这样在插入的时候，新节点可以放在任何位置，不会像线性结构那样插入一个元素，所有元素都需要向后排列。</p>
<p>下面是二叉查找树插入某个节点的动图演示：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/be13777664584209826f90d5d4128b7f.gif" alt="请添加图片描述"></p>
<p>因此，二叉查找树解决了连续结构插入新元素开销很大的问题，同时又保持着天然的二分结构。</p>
<p>那是不是二叉查找树就可以作为索引的数据结构了呢？</p>
<p>不行不行，二叉查找树存在一个极端情况，会导致它变成一个瘸子！</p>
<p>**当每次插入的元素都是二叉查找树中最大的元素，二叉查找树就会退化成了一条链表，查找数据的时间复杂度变成了 O(n)**，如下动图演示：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/320c5c2653e74f9e9ebfcd8ca9681905.gif" alt="请添加图片描述"></p>
<p>由于树是存储在磁盘中的，访问每个节点，都对应一次磁盘 I&#x2F;O 操作（<em>假设一个节点的大小「小于」操作系统的最小读写单位块的大小</em>），也就是说<strong>树的高度就等于每次查询数据时磁盘 IO 操作的次数</strong>，所以树的高度越高，就会影响查询性能。</p>
<p>二叉查找树由于存在退化成链表的可能性，会使得查询操作的时间复杂度从 O(logn) 升为 O(n)。</p>
<p>而且会随着插入的元素越多，树的高度也变高，意味着需要磁盘 IO 操作的次数就越多，这样导致查询性能严重下降，再加上不能范围查询，所以不适合作为数据库的索引结构。</p>
<h3 id="什么是自平衡二叉树？"><a href="#什么是自平衡二叉树？" class="headerlink" title="什么是自平衡二叉树？"></a>什么是自平衡二叉树？</h3><p>为了解决二叉查找树会在极端情况下退化成链表的问题，后面就有人提出<strong>平衡二叉查找树（AVL 树）</strong>。</p>
<p>主要是在二叉查找树的基础上增加了一些条件约束：<strong>每个节点的左子树和右子树的高度差不能超过 1</strong>。也就是说节点的左子树和右子树仍然为平衡二叉树，这样查询操作的时间复杂度就会一直维持在 O(logn) 。</p>
<p>下图是每次插入的元素都是平衡二叉查找树中最大的元素，可以看到，它会维持自平衡：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/8d5da86f2e41a0b641dbf45b23f7b7dd.gif" alt="图片"></p>
<p>除了平衡二叉查找树，还有很多自平衡的二叉树，比如红黑树，它也是通过一些约束条件来达到自平衡，不过红黑树的约束条件比较复杂，不是本篇的重点重点，大家可以看《数据结构》相关的书籍来了解红黑树的约束条件。</p>
<p>下面是<strong>红黑树</strong>插入节点的过程，这左旋右旋的操作，就是为了自平衡。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/b2628d1248e41207a08871f7bfac3522.gif" alt="图片"></p>
<p><strong>不管平衡二叉查找树还是红黑树，都会随着插入的元素增多，而导致树的高度变高，这就意味着磁盘 I&#x2F;O 操作次数多，会影响整体数据查询的效率</strong>。</p>
<p>比如，下面这个平衡二叉查找树的高度为 5，那么在访问最底部的节点时，就需要磁盘 5 次 I&#x2F;O 操作。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/2d26d30c953cd47c6ab637ad0eba2f99.png" alt="图片"></p>
<p>根本原因是因为它们都是二叉树，也就是每个节点只能保存 2 个子节点 ，如果我们把二叉树改成 M 叉树（M&gt;2）呢？</p>
<p>比如，当 M&#x3D;3 时，在同样的节点个数情况下，三叉树比二叉树的树高要矮。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/00fb73de7014a87958f1597345e9ef2f.png" alt="图片"></p>
<p>因此，<strong>当树的节点越多的时候，并且树的分叉数 M 越大的时候，M 叉树的高度会远小于二叉树的高度</strong>。</p>
<h3 id="什么是-B-树"><a href="#什么是-B-树" class="headerlink" title="什么是 B 树"></a>什么是 B 树</h3><p>自平衡二叉树虽然能保持查询操作的时间复杂度在O(logn)，但是因为它本质上是一个二叉树，每个节点只能有 2 个子节点，那么当节点个数越多的时候，树的高度也会相应变高，这样就会增加磁盘的 I&#x2F;O 次数，从而影响数据查询的效率。</p>
<p>为了解决降低树的高度的问题，后面就出来了 B 树，它不再限制一个节点就只能有 2 个子节点，而是允许 M 个子节点 (M&gt;2)，从而降低树的高度。</p>
<p>B 树的每一个节点最多可以包括 M 个子节点，M 称为 B 树的阶，所以 B 树就是一个多叉树。</p>
<p>假设 M &#x3D; 3，那么就是一棵 3 阶的 B 树，特点就是每个节点最多有 2 个（M-1个）数据和最多有 3 个（M个）子节点，超过这些要求的话，就会分裂节点，比如下面的的动图：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/9a96956de3be0614f7ec2344741b4dcc.gif" alt="图片"></p>
<p>我们来看看一棵 3 阶的 B 树的查询过程是怎样的？</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/341cedca6863fcd0624febd835165ae2.gif" alt="图片"></p>
<p>假设我们在上图一棵 3 阶的 B 树中要查找的索引值是 9 的记录那么步骤可以分为以下几步：</p>
<ol>
<li>与根节点的索引(4，8）进行比较，9 大于 8，那么往右边的子节点走；</li>
<li>然后该子节点的索引为（10，12），因为 9 小于 10，所以会往该节点的左边子节点走；</li>
<li>走到索引为9的节点，然后我们找到了索引值 9 的节点。</li>
</ol>
<p>可以看到，一棵 3 阶的 B 树在查询叶子节点中的数据时，由于树的高度是 3 ，所以在查询过程中会发生 3 次磁盘 I&#x2F;O 操作。</p>
<p>而如果同样的节点数量在平衡二叉树的场景下，树的高度就会很高，意味着磁盘 I&#x2F;O 操作会更多。所以，B 树在数据查询中比平衡二叉树效率要高。</p>
<p>但是 B 树的每个节点都包含数据（索引+记录），而用户的记录数据的大小很有可能远远超过了索引数据，这就需要花费更多的磁盘 I&#x2F;O 操作次数来读到「有用的索引数据」。</p>
<p>而且，在我们查询位于底层的某个节点（比如 A 记录）过程中，「非 A 记录节点」里的记录数据会从磁盘加载到内存，但是这些记录数据是没用的，我们只是想读取这些节点的索引数据来做比较查询，而「非 A 记录节点」里的记录数据对我们是没用的，这样不仅增多磁盘 I&#x2F;O 操作次数，也占用内存资源。</p>
<p>另外，如果使用 B 树来做范围查询的话，需要使用中序遍历，这会涉及多个节点的磁盘 I&#x2F;O 问题，从而导致整体速度下降。</p>
<h3 id="什么是-B-树？"><a href="#什么是-B-树？" class="headerlink" title="什么是 B+ 树？"></a>什么是 B+ 树？</h3><p>B+ 树就是对 B 树做了一个升级，MySQL 中索引的数据结构就是采用了 B+ 树，B+ 树结构如下图：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/b6678c667053a356f46fc5691d2f5878.png" alt="图片"></p>
<p>B+ 树与 B 树差异的点，主要是以下这几点：</p>
<ul>
<li>叶子节点（最底部的节点）才会存放实际数据（索引+记录），非叶子节点只会存放索引；</li>
<li>所有索引都会在叶子节点出现，叶子节点之间构成一个有序链表；</li>
<li>非叶子节点的索引也会同时存在在子节点中，并且是在子节点中所有索引的最大（或最小）。</li>
<li>非叶子节点中有多少个子节点，就有多少个索引；</li>
</ul>
<p>下面通过三个方面，比较下 B+ 和 B 树的性能区别。</p>
<h4 id="1、单点查询"><a href="#1、单点查询" class="headerlink" title="1、单点查询"></a>1、单点查询</h4><p>B 树进行单个索引查询时，最快可以在 O(1) 的时间代价内就查到，而从平均时间代价来看，会比 B+ 树稍快一些。</p>
<p>但是 B 树的查询波动会比较大，因为每个节点即存索引又存记录，所以有时候访问到了非叶子节点就可以找到索引，而有时需要访问到叶子节点才能找到索引。</p>
<p><strong>B+ 树的非叶子节点不存放实际的记录数据，仅存放索引，因此数据量相同的情况下，相比存储即存索引又存记录的 B 树，B+树的非叶子节点可以存放更多的索引，因此 B+ 树可以比 B 树更「矮胖」，查询底层节点的磁盘 I&#x2F;O次数会更少</strong>。</p>
<h4 id="2、插入和删除效率"><a href="#2、插入和删除效率" class="headerlink" title="2、插入和删除效率"></a>2、插入和删除效率</h4><p>B+ 树有大量的冗余节点，这样使得删除一个节点的时候，可以直接从叶子节点中删除，甚至可以不动非叶子节点，这样删除非常快，</p>
<p>比如下面这个动图是删除 B+ 树 0004 节点的过程，因为非叶子节点有 0004 的冗余节点，所以在删除的时候，树形结构变化很小：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/25508b0cd9c44ef6937fdd737020a7f1.gif" alt="请添加图片描述"></p>
<p><code>注意，：B+ 树对于非叶子节点的子节点和索引的个数，定义方式可能会有不同，有的是说非叶子节点的子节点的个数为 M 阶，而索引的个数为 M-1（这个是维基百科里的定义），因此本文关于 B+ 树的动图都是基于这个。但是我在前面介绍 B+ 树与 B+ 树的差异时，说的是「非叶子节点中有多少个子节点，就有多少个索引」，主要是 MySQL 用到的 B+ 树就是这个特性。</code></p>
<p>下面这个动图是删除 B 树 0008 节点的过程，可能会导致树的复杂变化：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/2be62679487640bbaac663fa96c7f35f.gif" alt="请添加图片描述"></p>
<p>甚至，B+ 树在删除根节点的时候，由于存在冗余的节点，所以不会发生复杂的树的变形，比如下面这个动图是删除 B+ 树根节点的过程：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/23730b5af987480fabff0f1d142a2b6c.gif" alt="请添加图片描述"></p>
<p>B 树则不同，B 树没有冗余节点，删除节点的时候非常复杂，比如删除根节点中的数据，可能涉及复杂的树的变形，比如下面这个动图是删除 B 树根节点的过程：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/7552002f9b8195ab650d431bfe66cce2.gif" alt="图片"></p>
<p>B+ 树的插入也是一样，有冗余节点，插入可能存在节点的分裂（如果节点饱和），但是最多只涉及树的一条路径。而且 B+ 树会自动平衡，不需要像更多复杂的算法，类似红黑树的旋转操作等。</p>
<p>因此，<strong>B+ 树的插入和删除效率更高</strong>。</p>
<h4 id="3、范围查询"><a href="#3、范围查询" class="headerlink" title="3、范围查询"></a>3、范围查询</h4><p>B 树和 B+ 树等值查询原理基本一致，先从根节点查找，然后对比目标数据的范围，最后递归的进入子节点查找。</p>
<p>因为 <strong>B+ 树所有叶子节点间还有一个链表进行连接，这种设计对范围查找非常有帮助</strong>，比如说我们想知道 12 月 1 日和 12 月 12 日之间的订单，这个时候可以先查找到 12 月 1 日所在的叶子节点，然后利用链表向右遍历，直到找到 12 月12 日的节点，这样就不需要从根节点查询了，进一步节省查询需要的时间。</p>
<p>而 B 树没有将所有叶子节点用链表串联起来的结构，因此只能通过树的遍历来完成范围查询，这会涉及多个节点的磁盘 I&#x2F;O 操作，范围查询效率不如 B+ 树。</p>
<p>因此，存在大量范围检索的场景，适合使用 B+树，比如数据库。而对于大量的单个索引查询的场景，可以考虑 B 树，比如 nosql 的MongoDB。</p>
<h3 id="MySQL-中的-B-树"><a href="#MySQL-中的-B-树" class="headerlink" title="MySQL 中的 B+ 树"></a>MySQL 中的 B+ 树</h3><p>MySQL 的存储方式根据存储引擎的不同而不同，我们最常用的就是 Innodb 存储引擎，它就是采用了 B+ 树作为了索引的数据结构。</p>
<p>下图就是 Innodb 里的 B+ 树：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/dd076212a7637b9032c97a615c39dcd7.png" alt="图片"></p>
<p>但是 Innodb 使用的 B+ 树有一些特别的点，比如：</p>
<ul>
<li>B+ 树的叶子节点之间是用「双向链表」进行连接，这样的好处是既能向右遍历，也能向左遍历。</li>
<li>B+ 树点节点内容是数据页，数据页里存放了用户的记录以及各种信息，每个数据页默认大小是 16 KB。</li>
</ul>
<p>Innodb 根据索引类型不同，分为聚集和二级索引。他们区别在于，聚集索引的叶子节点存放的是实际数据，所有完整的用户记录都存放在聚集索引的叶子节点，而二级索引的叶子节点存放的是主键值，而不是实际数据。</p>
<p>因为表的数据都是存放在聚集索引的叶子节点里，所以 InnoDB 存储引擎一定会为表创建一个聚集索引，且由于数据在物理上只会保存一份，所以聚簇索引只能有一个，而二级索引可以创建多个。</p>
<h3 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h3><p>MySQL 是会将数据持久化在硬盘，而<strong>存储功能是由 MySQL 存储引擎实现</strong>的，所以讨论 MySQL 使用哪种数据结构作为索引，实际上是在讨论存储引使用哪种数据结构作为索引，InnoDB 是 MySQL 默认的存储引擎，它就是采用了 B+ 树作为索引的数据结构。</p>
<p>要设计一个 MySQL 的索引数据结构，不仅仅考虑数据结构增删改的时间复杂度，更重要的是要考虑磁盘 I&#x2F;0 的操作次数。因为索引和记录都是存放在硬盘，硬盘是一个非常慢的存储设备，我们在查询数据的时候，最好能在尽可能少的磁盘 I&#x2F;0 的操作次数内完成。</p>
<p>二分查找树虽然是一个天然的二分结构，能很好的利用二分查找快速定位数据，但是它存在一种极端的情况，每当插入的元素都是树内最大的元素，就会导致二分查找树退化成一个链表，此时查询复杂度就会从 O(logn)降低为 O(n)。</p>
<p>为了解决二分查找树退化成链表的问题，就出现了自平衡二叉树，保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树，每个节点只能有 2 个子节点，随着元素的增多，树的高度会越来越高。</p>
<p>而树的高度决定于磁盘 I&#x2F;O 操作的次数，因为树是存储在磁盘中的，访问每个节点，都对应一次磁盘 I&#x2F;O 操作，也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数，所以树的高度越高，就会影响查询性能。</p>
<p>B 树和 B+ 都是通过多叉树的方式，会将树的高度变矮，所以这两个数据结构非常适合检索存于磁盘中的数据。</p>
<p>但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构，原因有：</p>
<ul>
<li>B+ 树的非叶子节点不存放实际的记录数据，仅存放索引，因此数据量相同的情况下，相比即存储存索引又存记录的 B 树，B+树的非叶子节点可以存放更多的索引，因此 B+ 树可以比 B 树更「矮胖」，查询底层节点的磁盘 I&#x2F;O次数会更少。</li>
<li>B+ 树有大量的冗余节点（所有非叶子节点都是冗余索引），这些冗余索引让 B+ 树在插入、删除的效率都更高，比如删除根节点的时候，不会像 B 树那样会发生复杂的树的变化；</li>
<li>B+ 树叶子节点之间用链表连接了起来，有利于范围查询，而 B 树要实现范围查询，因此只能通过树的遍历来完成范围查询，这会涉及多个节点的磁盘 I&#x2F;O 操作，范围查询效率不如 B+ 树。</li>
</ul>
<h2 id="3-MySQL-单表不要超过-2000W-行，靠谱吗？"><a href="#3-MySQL-单表不要超过-2000W-行，靠谱吗？" class="headerlink" title="3. MySQL 单表不要超过 2000W 行，靠谱吗？"></a>3. MySQL 单表不要超过 2000W 行，靠谱吗？</h2><ul>
<li>MySQL 的表数据是以页的形式存放的，页在磁盘中不一定是连续的。</li>
<li>页的空间是 16K, 并不是所有的空间都是用来存放数据的，会有一些固定的信息，如，页头，页尾，页码，校验码等等。</li>
<li>在 B+ 树中，叶子节点和非叶子节点的数据结构是一样的，区别在于，叶子节点存放的是实际的行数据，而非叶子节点存放的是主键和页号。</li>
<li>索引结构不会影响单表最大行数，2000W 也只是推荐值，超过了这个值可能会导致 B + 树层级更高，影响查询性能。</li>
</ul>
<h2 id="4-索引失效有哪些？"><a href="#4-索引失效有哪些？" class="headerlink" title="4. 索引失效有哪些？"></a>4. 索引失效有哪些？</h2><ul>
<li>当我们使用左或者左右模糊匹配的时候，也就是 <code>like %xx</code> 或者 <code>like %xx%</code>这两种方式都会造成索引失效；</li>
<li>当我们在查询条件中对索引列使用函数，就会导致索引失效。</li>
<li>当我们在查询条件中对索引列进行表达式计算，也是无法走索引的。</li>
<li>MySQL 在遇到字符串和数字比较的时候，会自动把字符串转为数字，然后再进行比较。如果字符串是索引列，而条件语句中的输入参数是数字的话，那么索引列会发生隐式类型转换，由于隐式类型转换是通过 CAST 函数实现的，等同于对索引列使用了函数，所以就会导致索引失效。</li>
<li>联合索引要能正确使用需要遵循最左匹配原则，也就是按照最左优先的方式进行索引的匹配，否则就会导致索引失效。</li>
<li>在 WHERE 子句中，如果在 OR 前的条件列是索引列，而在 OR 后的条件列不是索引列，那么索引会失效。</li>
</ul>
<p>&#x3D;&#x3D;<strong>注意：</strong>(使用左模糊匹配（like “%xx”）并不一定会走全表扫描，关键还是看数据表中的字段。如果数据库表中的字段只有主键+二级索引，那么即使使用了左模糊匹配，也不会走全表扫描（type&#x3D;all），而是走全扫描二级索引树(type&#x3D;index)。)&#x3D;&#x3D;</p>
<h2 id="5-count-和-count-1-有什么区别？哪个性能最好？"><a href="#5-count-和-count-1-有什么区别？哪个性能最好？" class="headerlink" title="5. count(*) 和 count(1) 有什么区别？哪个性能最好？"></a>5. count(*) 和 count(1) 有什么区别？哪个性能最好？</h2><p>当我们对一张数据表中的记录进行统计的时候，习惯都会使用 count 函数来统计，但是 count 函数传入的参数有很多种，比如 count(1)、count(<code>*</code>)、count(字段) 等。</p>
<p>到底哪种效率是最好的呢？是不是 count(<code>*</code>) 效率最差？</p>
<p>我曾经以为 count(<code>*</code>) 是效率最差的，因为认知上 <code>selete * from t</code> 会读取所有表中的字段，所以凡事带有 <code>*</code> 字符的就觉得会读取表中所有的字段，当时网上有很多博客也这么说。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/d9b9817e92f805e9a16faf31a2c10d9a.png" alt="图片"></p>
<h3 id="哪种-count-性能最好？"><a href="#哪种-count-性能最好？" class="headerlink" title="哪种 count 性能最好？"></a>哪种 count 性能最好？</h3><p><img src="https://cdn.xiaolincoding.com//mysql/other/af711033aa3423330d3a4bc6baeb9532.png" alt="图片"></p>
<p>要弄明白这个，我们得要深入 count 的原理，以下内容基于常用的 innodb 存储引擎来说明。</p>
<h3 id="count-是什么？"><a href="#count-是什么？" class="headerlink" title="count() 是什么？"></a>count() 是什么？</h3><p>count() 是一个聚合函数，函数的参数不仅可以是字段名，也可以是其他任意表达式，该函数作用是<strong>统计符合查询条件的记录中，函数指定的参数不为 NULL 的记录有多少个</strong>。</p>
<p>假设 count() 函数的参数是字段名，如下：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select count(name) from t_order;</span><br></pre></td></tr></table></figure>

<p>这条语句是统计「 t_order 表中，name 字段不为 NULL 的记录」有多少个。也就是说，如果某一条记录中的 name 字段的值为 NULL，则就不会被统计进去。</p>
<p>再来假设 count() 函数的参数是数字 1 这个表达式，如下：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select count(1) from t_order;</span><br></pre></td></tr></table></figure>

<p>这条语句是统计「 t_order 表中，1 这个表达式不为 NULL 的记录」有多少个。</p>
<p>1 这个表达式就是单纯数字，它永远都不是 NULL，所以上面这条语句，其实是在统计 t_order 表中有多少个记录。</p>
<h3 id="count-主键字段-执行过程是怎样的？"><a href="#count-主键字段-执行过程是怎样的？" class="headerlink" title="count(主键字段) 执行过程是怎样的？"></a>count(主键字段) 执行过程是怎样的？</h3><p>在通过 count 函数统计有多少个记录时，MySQL 的 server 层会维护一个名叫 count 的变量。</p>
<p>server 层会循环向 InnoDB 读取一条记录，如果 count 函数指定的参数不为 NULL，那么就会将变量 count 加 1，直到符合查询的全部记录被读完，就退出循环。最后将 count 变量的值发送给客户端。</p>
<p>InnoDB 是通过 B+ 树来保存记录的，根据索引的类型又分为聚簇索引和二级索引，它们区别在于，聚簇索引的叶子节点存放的是实际数据，而二级索引的叶子节点存放的是主键值，而不是实际数据。</p>
<p>用下面这条语句作为例子：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">//id 为主键值</span><br><span class="line">select count(id) from t_order;</span><br></pre></td></tr></table></figure>

<p>如果表里只有主键索引，没有二级索引时，那么，InnoDB 循环遍历聚簇索引，将读取到的记录返回给 server 层，然后读取记录中的 id 值，就会 id 值判断是否为 NULL，如果不为 NULL，就将 count 变量加 1。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/9bb4f32ac843467684a2664d4db61ae3.png" alt="图片"></p>
<p>但是，如果表里有二级索引时，InnoDB 循环遍历的对象就不是聚簇索引，而是二级索引。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/aac550602ef1022e0b45020dbe0f716a.png" alt="图片"></p>
<p>这是因为相同数量的二级索引记录可以比聚簇索引记录占用更少的存储空间，所以二级索引树比聚簇索引树小，这样遍历二级索引的 I&#x2F;O 成本比遍历聚簇索引的 I&#x2F;O 成本小，因此「优化器」优先选择的是二级索引。</p>
<h3 id="count-1-执行过程是怎样的？"><a href="#count-1-执行过程是怎样的？" class="headerlink" title="count(1) 执行过程是怎样的？"></a>count(1) 执行过程是怎样的？</h3><p>用下面这条语句作为例子：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select count(1) from t_order;</span><br></pre></td></tr></table></figure>

<p>如果表里只有主键索引，没有二级索引时。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/e630fdc5897b5c5dbc332e8838afa1fc.png" alt="图片"></p>
<p>那么，InnoDB 循环遍历聚簇索引（主键索引），将读取到的记录返回给 server 层，<strong>但是不会读取记录中的任何字段的值</strong>，因为 count 函数的参数是 1，不是字段，所以不需要读取记录中的字段值。参数 1 很明显并不是 NULL，因此 server 层每从 InnoDB 读取到一条记录，就将 count 变量加 1。</p>
<p>可以看到，count(1) 相比 count(主键字段) 少一个步骤，就是不需要读取记录中的字段值，所以通常会说 count(1) 执行效率会比 count(主键字段) 高一点。</p>
<p>但是，如果表里有二级索引时，InnoDB 循环遍历的对象就二级索引了。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/01e83441a7721f0864deb1ac14ad8ea6.png" alt="图片"></p>
<h3 id="count-执行过程是怎样的？"><a href="#count-执行过程是怎样的？" class="headerlink" title="count(*) 执行过程是怎样的？"></a>count(*) 执行过程是怎样的？</h3><p>看到 <code>*</code> 这个字符的时候，是不是大家觉得是读取记录中的所有字段值？</p>
<p>对于 <code>selete *</code> 这条语句来说是这个意思，但是在 count(*) 中并不是这个意思。</p>
<p>**count(*) 其实等于 count(0)**，也就是说，当你使用 count(<code>*</code>) 时，MySQL 会将 <code>*</code> 参数转化为参数 0 来处理。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/27b229f049b27898f3a86c7da7e26114.png" alt="图片"></p>
<p>所以，<strong>count(*) 执行过程跟 count(1) 执行过程基本一样的</strong>，性能没有什么差异。</p>
<p>在 MySQL 5.7 的官方手册中有这么一句话：</p>
<p><em>InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.</em></p>
<p><em>翻译：InnoDB以相同的方式处理SELECT COUNT（*）和SELECT COUNT（1）操作，没有性能差异。</em></p>
<p>而且 MySQL 会对 count(*) 和 count(1) 有个优化，如果有多个二级索引的时候，优化器会使用key_len 最小的二级索引进行扫描。</p>
<p>只有当没有二级索引的时候，才会采用主键索引来进行统计。</p>
<h3 id="count-字段-执行过程是怎样的？"><a href="#count-字段-执行过程是怎样的？" class="headerlink" title="count(字段) 执行过程是怎样的？"></a>count(字段) 执行过程是怎样的？</h3><p>count(字段) 的执行效率相比前面的 count(1)、 count(*)、 count(主键字段) 执行效率是最差的。</p>
<p>用下面这条语句作为例子：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">// name不是索引，普通字段</span><br><span class="line">select count(name) from t_order;</span><br></pre></td></tr></table></figure>

<p>对于这个查询来说，会采用全表扫描的方式来计数，所以它的执行效率是比较差的。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/f24dfeb85e2cfce0e4dc3a17b893b3f5.png" alt="图片"></p>
<h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>count(1)、 count(*)、 count(主键字段)在执行的时候，如果表里存在二级索引，优化器就会选择二级索引进行扫描。</p>
<p>所以，如果要执行 count(1)、 count(*)、 count(主键字段) 时，尽量在数据表上建立二级索引，这样优化器会自动采用 key_len 最小的二级索引进行扫描，相比于扫描主键索引效率会高一些。</p>
<p>再来，就是不要使用 count(字段) 来统计记录个数，因为它的效率是最差的，会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数，建议给这个字段建立一个二级索引。</p>
<h3 id="为什么要通过遍历的方式来计数？"><a href="#为什么要通过遍历的方式来计数？" class="headerlink" title="为什么要通过遍历的方式来计数？"></a>为什么要通过遍历的方式来计数？</h3><p>你可能会好奇，为什么 count 函数需要通过遍历的方式来统计记录个数？</p>
<p>我前面将的案例都是基于 Innodb 存储引擎来说明的，但是在 MyISAM 存储引擎里，执行 count 函数的方式是不一样的，通常在没有任何查询条件下的 count(*)，MyISAM 的查询速度要明显快于 InnoDB。</p>
<p>使用 MyISAM 引擎时，执行 count 函数只需要 O(1 )复杂度，这是因为每张 MyISAM 的数据表都有一个 meta 信息有存储了row_count值，由<strong>表级锁</strong>保证一致性，所以直接读取 row_count 值就是 count 函数的执行结果。</p>
<p><strong>而 InnoDB 存储引擎是支持事务的，同一个时刻的多个查询，由于多版本并发控制（MVCC）的原因，InnoDB 表“应该返回多少行”也是不确定的，所以无法像 MyISAM一样，只维护一个 row_count 变量。</strong></p>
<p>举个例子，假设表 t_order 有 100 条记录，现在有两个会话并行以下语句：</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/04d714293f5c687810562e984b67d2e7.png" alt="图片"></p>
<p>在会话 A 和会话 B的最后一个时刻，同时查表 t_order 的记录总个数，可以发现，显示的结果是不一样的。所以，在使用 InnoDB 存储引擎时，就需要扫描表来统计具体的记录。</p>
<p>而当带上 where 条件语句之后，MyISAM 跟 InnoDB 就没有区别了，它们都需要扫描表来进行记录个数的统计。</p>
<h3 id="如何优化-count-？"><a href="#如何优化-count-？" class="headerlink" title="如何优化 count(*)？"></a>如何优化 count(*)？</h3><p>如果对一张大表经常用 count(*) 来做统计，其实是很不好的。</p>
<p>比如下面我这个案例，表 t_order 共有 1200+ 万条记录，我也创建了二级索引，但是执行一次 <code>select count(*) from t_order</code> 要花费差不多 5 秒！</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/74a4359b58dc6ed41a241e425f43764d.png" alt="图片"></p>
<p>面对大表的记录统计，我们有没有什么其他更好的办法呢？</p>
<h4 id="第一种，近似值"><a href="#第一种，近似值" class="headerlink" title="第一种，近似值"></a>第一种，近似值</h4><p>如果你的业务对于统计个数不需要很精确，比如搜索引擎在搜索关键词的时候，给出的搜索结果条数是一个大概值。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/cd18879de0c0b37660f53a5f1af3d172.png" alt="图片"></p>
<p>这时，我们就可以使用 show table status 或者 explain 命令来表进行估算。</p>
<p>执行 explain 命令效率是很高的，因为它并不会真正的去查询，下图中的 rows 字段值就是 explain 命令对表 t_order 记录的估算值。</p>
<p><img src="https://cdn.xiaolincoding.com//mysql/other/7590623443e8f225e5652109e6d9e3d2.png" alt="图片"></p>
<h4 id="第二种，额外表保存计数值"><a href="#第二种，额外表保存计数值" class="headerlink" title="第二种，额外表保存计数值"></a>第二种，额外表保存计数值</h4><p>如果是想精确的获取表的记录总数，我们可以将这个计数值保存到单独的一张计数表中。</p>
<p>当我们在数据表插入一条记录的同时，将计数表中的计数字段 + 1。也就是说，在新增和删除操作时，我们需要额外维护这个计数表。</p>
</article><div class="post-copyright"><div class="post-copyright__author"><span class="post-copyright-meta">Author: </span><span class="post-copyright-info"><a href="https://kirito2493381254.gitee.io/kirito">牵风散步の雲</a></span></div><div class="post-copyright__type"><span class="post-copyright-meta">Link: </span><span class="post-copyright-info"><a href="https://kirito2493381254.gitee.io/kirito/2023/08/09/MySQL%E7%B4%A2%E5%BC%95%E7%AF%87/">https://kirito2493381254.gitee.io/kirito/2023/08/09/MySQL%E7%B4%A2%E5%BC%95%E7%AF%87/</a></span></div><div class="post-copyright__notice"><span class="post-copyright-meta">Copyright Notice: </span><span class="post-copyright-info">All articles in this blog are licensed under <a target="_blank" rel="noopener" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> unless stating additionally.</span></div></div><div class="tag_share"><div class="post-meta__tag-list"><a class="post-meta__tags" href="/kirito/tags/MySQL%E7%B4%A2%E5%BC%95%E7%AF%87/">MySQL索引篇</a></div><div class="post_share"><div class="social-share" data-image="/kirito/img/artical5.jpg" data-sites="facebook,twitter,wechat,weibo,qq"></div><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/css/share.min.css" media="print" onload="this.media='all'"><script src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/sharejs/dist/js/social-share.min.js" defer></script></div></div><nav class="pagination-post" id="pagination"><div class="prev-post pull-left"><a href="/kirito/2023/08/09/MySQL%E4%BA%8B%E5%8A%A1%E7%AF%87/" title="MySQL事务"><img class="cover" src="/kirito/img/artical3.jpg" onerror="onerror=null;src='/kirito/img/404.jpg'" alt="cover of previous post"><div class="pagination-info"><div class="label">Previous Post</div><div class="prev_info">MySQL事务</div></div></a></div><div class="next-post pull-right"><a href="/kirito/2023/08/04/go-zero/" title="go-zero"><img class="cover" src="/kirito/img/artical.jpg" onerror="onerror=null;src='/kirito/img/404.jpg'" alt="cover of next post"><div class="pagination-info"><div class="label">Next Post</div><div class="next_info">go-zero</div></div></a></div></nav><hr class="custom-hr"/><div id="post-comment"><div class="comment-head"><div class="comment-headline"><i class="fas fa-comments fa-fw"></i><span> Comment</span></div></div><div class="comment-wrap"><div><div class="vcomment" id="vcomment"></div></div></div></div></div><div class="aside-content" id="aside-content"><div class="card-widget card-info"><div class="is-center"><div class="avatar-img"><img src="/kirito/img/avatar.jpg" onerror="this.onerror=null;this.src='/kirito/img/friend_404.gif'" alt="avatar"/></div><div class="author-info__name">牵风散步の雲</div><div class="author-info__description"></div></div><div class="card-info-data site-data is-center"><a href="/kirito/archives/"><div class="headline">Articles</div><div class="length-num">21</div></a><a href="/kirito/tags/"><div class="headline">Tags</div><div class="length-num">12</div></a><a href="/kirito/categories/"><div class="headline">Categories</div><div class="length-num">8</div></a></div><a id="card-info-btn" target="_blank" rel="noopener" href="https://github.com/Kiritoabc"><i class="fab fa-github"></i><span>Follow Me</span></a><div class="card-info-social-icons is-center"><a class="social-icon" href="https://github.com/Kiritoabc" target="_blank" title="Github"><i class="fab fa-github" style="color: #24292e;"></i></a><a class="social-icon" href="mailto:2493381254@qq.com" target="_blank" title="Email"><i class="fas fa-envelope" style="color: #4a7dbe;"></i></a></div></div><div class="card-widget card-announcement"><div class="item-headline"><i class="fas fa-bullhorn fa-shake"></i><span>Announcement</span></div><div class="announcement_content">This is my Blog</div></div><div class="sticky_layout"><div class="card-widget" id="card-toc"><div class="item-headline"><i class="fas fa-stream"></i><span>Catalog</span><span class="toc-percentage"></span></div><div class="toc-content"><ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#MySQL%E7%B4%A2%E5%BC%95%E7%AF%87"><span class="toc-number">1.</span> <span class="toc-text">MySQL索引篇</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#1-%E4%BB%8E%E6%95%B0%E6%8D%AE%E9%A1%B5%E7%9A%84%E8%A7%92%E5%BA%A6%E7%9C%8B-B-%E6%A0%91"><span class="toc-number">1.1.</span> <span class="toc-text">1. 从数据页的角度看 B+ 树</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#InnoDB-%E6%98%AF%E5%A6%82%E4%BD%95%E5%AD%98%E5%82%A8%E6%95%B0%E6%8D%AE%E7%9A%84%EF%BC%9F"><span class="toc-number">1.1.1.</span> <span class="toc-text">InnoDB 是如何存储数据的？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#B-%E6%A0%91%E6%98%AF%E5%A6%82%E4%BD%95%E8%BF%9B%E8%A1%8C%E6%9F%A5%E8%AF%A2%E7%9A%84%EF%BC%9F"><span class="toc-number">1.1.2.</span> <span class="toc-text">B+ 树是如何进行查询的？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95%E5%92%8C%E4%BA%8C%E7%BA%A7%E7%B4%A2%E5%BC%95"><span class="toc-number">1.1.3.</span> <span class="toc-text">聚簇索引和二级索引</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%80%BB%E7%BB%93"><span class="toc-number">1.1.4.</span> <span class="toc-text">总结</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#2-%E4%B8%BA%E4%BB%80%E4%B9%88-MySQL-%E9%87%87%E7%94%A8-B-%E6%A0%91%E4%BD%9C%E4%B8%BA%E7%B4%A2%E5%BC%95%EF%BC%9F"><span class="toc-number">1.2.</span> <span class="toc-text">2. 为什么 MySQL 采用 B+ 树作为索引？</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%80%8E%E6%A0%B7%E7%9A%84%E7%B4%A2%E5%BC%95%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E6%98%AF%E5%A5%BD%E7%9A%84%EF%BC%9F"><span class="toc-number">1.2.1.</span> <span class="toc-text">怎样的索引的数据结构是好的？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%EF%BC%9F"><span class="toc-number">1.2.2.</span> <span class="toc-text">什么是二分查找？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%A0%91%EF%BC%9F"><span class="toc-number">1.2.3.</span> <span class="toc-text">什么是二分查找树？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF%E8%87%AA%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91%EF%BC%9F"><span class="toc-number">1.2.4.</span> <span class="toc-text">什么是自平衡二叉树？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF-B-%E6%A0%91"><span class="toc-number">1.2.5.</span> <span class="toc-text">什么是 B 树</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF-B-%E6%A0%91%EF%BC%9F"><span class="toc-number">1.2.6.</span> <span class="toc-text">什么是 B+ 树？</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#1%E3%80%81%E5%8D%95%E7%82%B9%E6%9F%A5%E8%AF%A2"><span class="toc-number">1.2.6.1.</span> <span class="toc-text">1、单点查询</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#2%E3%80%81%E6%8F%92%E5%85%A5%E5%92%8C%E5%88%A0%E9%99%A4%E6%95%88%E7%8E%87"><span class="toc-number">1.2.6.2.</span> <span class="toc-text">2、插入和删除效率</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#3%E3%80%81%E8%8C%83%E5%9B%B4%E6%9F%A5%E8%AF%A2"><span class="toc-number">1.2.6.3.</span> <span class="toc-text">3、范围查询</span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#MySQL-%E4%B8%AD%E7%9A%84-B-%E6%A0%91"><span class="toc-number">1.2.7.</span> <span class="toc-text">MySQL 中的 B+ 树</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%80%BB%E7%BB%93-1"><span class="toc-number">1.2.8.</span> <span class="toc-text">总结</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#3-MySQL-%E5%8D%95%E8%A1%A8%E4%B8%8D%E8%A6%81%E8%B6%85%E8%BF%87-2000W-%E8%A1%8C%EF%BC%8C%E9%9D%A0%E8%B0%B1%E5%90%97%EF%BC%9F"><span class="toc-number">1.3.</span> <span class="toc-text">3. MySQL 单表不要超过 2000W 行，靠谱吗？</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#4-%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%9F"><span class="toc-number">1.4.</span> <span class="toc-text">4. 索引失效有哪些？</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#5-count-%E5%92%8C-count-1-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%EF%BC%9F%E5%93%AA%E4%B8%AA%E6%80%A7%E8%83%BD%E6%9C%80%E5%A5%BD%EF%BC%9F"><span class="toc-number">1.5.</span> <span class="toc-text">5. count(*) 和 count(1) 有什么区别？哪个性能最好？</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%93%AA%E7%A7%8D-count-%E6%80%A7%E8%83%BD%E6%9C%80%E5%A5%BD%EF%BC%9F"><span class="toc-number">1.5.1.</span> <span class="toc-text">哪种 count 性能最好？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#count-%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F"><span class="toc-number">1.5.2.</span> <span class="toc-text">count() 是什么？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#count-%E4%B8%BB%E9%94%AE%E5%AD%97%E6%AE%B5-%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F"><span class="toc-number">1.5.3.</span> <span class="toc-text">count(主键字段) 执行过程是怎样的？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#count-1-%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F"><span class="toc-number">1.5.4.</span> <span class="toc-text">count(1) 执行过程是怎样的？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#count-%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F"><span class="toc-number">1.5.5.</span> <span class="toc-text">count(*) 执行过程是怎样的？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#count-%E5%AD%97%E6%AE%B5-%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%EF%BC%9F"><span class="toc-number">1.5.6.</span> <span class="toc-text">count(字段) 执行过程是怎样的？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%B0%8F%E7%BB%93"><span class="toc-number">1.5.7.</span> <span class="toc-text">小结</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E9%80%9A%E8%BF%87%E9%81%8D%E5%8E%86%E7%9A%84%E6%96%B9%E5%BC%8F%E6%9D%A5%E8%AE%A1%E6%95%B0%EF%BC%9F"><span class="toc-number">1.5.8.</span> <span class="toc-text">为什么要通过遍历的方式来计数？</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%A6%82%E4%BD%95%E4%BC%98%E5%8C%96-count-%EF%BC%9F"><span class="toc-number">1.5.9.</span> <span class="toc-text">如何优化 count(*)？</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#%E7%AC%AC%E4%B8%80%E7%A7%8D%EF%BC%8C%E8%BF%91%E4%BC%BC%E5%80%BC"><span class="toc-number">1.5.9.1.</span> <span class="toc-text">第一种，近似值</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E7%AC%AC%E4%BA%8C%E7%A7%8D%EF%BC%8C%E9%A2%9D%E5%A4%96%E8%A1%A8%E4%BF%9D%E5%AD%98%E8%AE%A1%E6%95%B0%E5%80%BC"><span class="toc-number">1.5.9.2.</span> <span class="toc-text">第二种，额外表保存计数值</span></a></li></ol></li></ol></li></ol></li></ol></div></div><div class="card-widget card-recent-post"><div class="item-headline"><i class="fas fa-history"></i><span>Recent Post</span></div><div class="aside-list"><div class="aside-list-item"><a class="thumbnail" href="/kirito/2023/08/09/MySQL%E4%BA%8B%E5%8A%A1%E7%AF%87/" title="MySQL事务"><img src="/kirito/img/artical3.jpg" onerror="this.onerror=null;this.src='/kirito/img/404.jpg'" alt="MySQL事务"/></a><div class="content"><a class="title" href="/kirito/2023/08/09/MySQL%E4%BA%8B%E5%8A%A1%E7%AF%87/" title="MySQL事务">MySQL事务</a><time datetime="2023-08-09T10:21:17.207Z" title="Created 2023-08-09 18:21:17">2023-08-09</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/kirito/2023/08/09/MySQL%E7%B4%A2%E5%BC%95%E7%AF%87/" title="MySQL索引"><img src="/kirito/img/artical5.jpg" onerror="this.onerror=null;this.src='/kirito/img/404.jpg'" alt="MySQL索引"/></a><div class="content"><a class="title" href="/kirito/2023/08/09/MySQL%E7%B4%A2%E5%BC%95%E7%AF%87/" title="MySQL索引">MySQL索引</a><time datetime="2023-08-09T08:20:58.443Z" title="Created 2023-08-09 16:20:58">2023-08-09</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/kirito/2023/08/04/go-zero/" title="go-zero"><img src="/kirito/img/artical.jpg" onerror="this.onerror=null;this.src='/kirito/img/404.jpg'" alt="go-zero"/></a><div class="content"><a class="title" href="/kirito/2023/08/04/go-zero/" title="go-zero">go-zero</a><time datetime="2023-08-04T08:43:36.925Z" title="Created 2023-08-04 16:43:36">2023-08-04</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/kirito/2023/06/26/Canal/" title="Canal"><img src="/kirito/img/artical4.jpg" onerror="this.onerror=null;this.src='/kirito/img/404.jpg'" alt="Canal"/></a><div class="content"><a class="title" href="/kirito/2023/06/26/Canal/" title="Canal">Canal</a><time datetime="2023-06-26T05:53:08.133Z" title="Created 2023-06-26 13:53:08">2023-06-26</time></div></div><div class="aside-list-item"><a class="thumbnail" href="/kirito/2023/06/19/Lua/" title="lua"><img src="/kirito/img/artical2.jpg" onerror="this.onerror=null;this.src='/kirito/img/404.jpg'" alt="lua"/></a><div class="content"><a class="title" href="/kirito/2023/06/19/Lua/" title="lua">lua</a><time datetime="2023-06-19T04:42:42.400Z" title="Created 2023-06-19 12:42:42">2023-06-19</time></div></div></div></div></div></div></main><footer id="footer" style="background-image: url('/kirito/img/artical5.jpg')"><div id="footer-wrap"><div class="copyright">&copy;2023 By 牵风散步の雲</div><div class="framework-info"><span>Framework </span><a target="_blank" rel="noopener" href="https://hexo.io">Hexo</a><span class="footer-separator">|</span><span>Theme </span><a target="_blank" rel="noopener" href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly</a></div></div></footer></div><div id="rightside"><div id="rightside-config-hide"><button id="readmode" type="button" title="Read Mode"><i class="fas fa-book-open"></i></button><button id="darkmode" type="button" title="Switch Between Light And Dark Mode"><i class="fas fa-adjust"></i></button><button id="hide-aside-btn" type="button" title="Toggle between single-column and double-column"><i class="fas fa-arrows-alt-h"></i></button></div><div id="rightside-config-show"><button id="rightside_config" type="button" title="Setting"><i class="fas fa-cog fa-spin"></i></button><button class="close" id="mobile-toc-button" type="button" title="Table Of Contents"><i class="fas fa-list-ul"></i></button><button id="chat-btn" type="button" title="Chat"><i class="fas fa-sms"></i></button><a id="to_comment" href="#post-comment" title="Scroll To Comments"><i class="fas fa-comments"></i></a><button id="go-up" type="button" title="Back To Top"><span class="scroll-percent"></span><i class="fas fa-arrow-up"></i></button></div></div><div><script src="/kirito/js/utils.js"></script><script src="/kirito/js/main.js"></script><script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox/fancybox.umd.min.js"></script><div class="js-pjax"><script>function loadValine () {
  function initValine () {
    const valine = new Valine(Object.assign({
      el: '#vcomment',
      appId: '',
      appKey: '',
      avatar: 'monsterid',
      serverURLs: '',
      emojiMaps: "",
      path: window.location.pathname,
      visitor: false
    }, null))
  }

  if (typeof Valine === 'function') initValine() 
  else getScript('https://cdn.jsdelivr.net/npm/valine/dist/Valine.min.js').then(initValine)
}

if ('Valine' === 'Valine' || !true) {
  if (true) btf.loadComment(document.getElementById('vcomment'),loadValine)
  else setTimeout(loadValine, 0)
} else {
  function loadOtherComment () {
    loadValine()
  }
}</script></div><script src="https://cdn.jsdelivr.net/npm/butterfly-extsrc/dist/activate-power-mode.min.js"></script><script>POWERMODE.colorful = true;
POWERMODE.shake = true;
POWERMODE.mobile = false;
document.body.addEventListener('input', POWERMODE);
</script><script async data-pjax src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script></div></body></html>